/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.pdfbox.rendering;

import java.awt.Color;
import java.awt.DisplayMode;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup;
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.pdmodel.interactive.annotation.AnnotationFilter;

/**
 * Renders a PDF document to an AWT BufferedImage.
 * This class may be overridden in order to perform custom rendering.
 *
 * @author John Hewson
 */
public class PDFRenderer {
    private static final Log LOG = LogFactory.getLog(PDFRenderer.class);

    protected final PDDocument document;
    // TODO keep rendering state such as caches here

    /**
     * Default annotations filter, returns all annotations
     */
    private AnnotationFilter annotationFilter = annotation -> true;

    private boolean subsamplingAllowed = false;

    private RenderDestination defaultDestination;

    private RenderingHints renderingHints = null;

    private BufferedImage pageImage;

    private static boolean kcmsLogged = false;

    /**
     * Creates a new PDFRenderer.
     *
     * @param document the document to render
     */
    public PDFRenderer(PDDocument document) {
        this.document = document;

        if (!kcmsLogged) {
            suggestKCMS();
            kcmsLogged = true;
        }
    }

    /**
     * Return the AnnotationFilter.
     *
     * @return the AnnotationFilter
     */
    public AnnotationFilter getAnnotationsFilter() {
        return annotationFilter;
    }

    /**
     * Set the AnnotationFilter.
     *
     * <p>Allows to only render annotation accepted by the filter.
     *
     * @param annotationsFilter the AnnotationFilter
     */
    public void setAnnotationsFilter(AnnotationFilter annotationsFilter) {
        this.annotationFilter = annotationsFilter;
    }

    /**
     * Value indicating if the renderer is allowed to subsample images before drawing, according to
     * image dimensions and requested scale.
     * <p>
     * Subsampling may be faster and less memory-intensive in some cases, but it may also lead to
     * loss of quality, especially in images with high spatial frequency.
     *
     * @return true if subsampling of images is allowed, false otherwise.
     */
    public boolean isSubsamplingAllowed() {
        return subsamplingAllowed;
    }

    /**
     * Sets a value instructing the renderer whether it is allowed to subsample images before
     * drawing. The subsampling frequency is determined according to image size and requested scale.
     * <p>
     * Subsampling may be faster and less memory-intensive in some cases, but it may also lead to
     * loss of quality, especially in images with high spatial frequency.
     *
     * @param subsamplingAllowed The new value indicating if subsampling is allowed.
     */
    public void setSubsamplingAllowed(boolean subsamplingAllowed) {
        this.subsamplingAllowed = subsamplingAllowed;
    }

    /**
     * @return the defaultDestination
     */
    public RenderDestination getDefaultDestination() {
        return defaultDestination;
    }

    /**
     * @param defaultDestination the defaultDestination to set
     */
    public void setDefaultDestination(RenderDestination defaultDestination) {
        this.defaultDestination = defaultDestination;
    }

    /**
     * Get the rendering hints.
     *
     * @return the rendering hints or null if none are set.
     */
    public RenderingHints getRenderingHints() {
        return renderingHints;
    }

    /**
     * Set the rendering hints. Use this to influence rendering quality and speed. If you don't set
     * them yourself or pass null, PDFBox will decide <b><u>at runtime</u></b> depending on the
     * destination.
     *
     * @param renderingHints
     */
    public void setRenderingHints(RenderingHints renderingHints) {
        this.renderingHints = renderingHints;
    }

    /**
     * Returns the given page as an RGB image at 72 DPI
     *
     * @param pageIndex the zero-based index of the page to be converted.
     * @return the rendered page image
     * @throws IOException if the PDF cannot be read
     */
    public BufferedImage renderImage(int pageIndex) throws IOException {
        return renderImage(pageIndex, 1);
    }

    /**
     * Returns the given page as an RGB image at the given scale.
     * A scale of 1 will render at 72 DPI.
     *
     * @param pageIndex the zero-based index of the page to be converted
     * @param scale     the scaling factor, where 1 = 72 DPI
     * @return the rendered page image
     * @throws IOException if the PDF cannot be read
     */
    public BufferedImage renderImage(int pageIndex, float scale) throws IOException {
        return renderImage(pageIndex, scale, ImageType.RGB);
    }

    /**
     * Returns the given page as an RGB image at the given DPI.
     *
     * @param pageIndex the zero-based index of the page to be converted
     * @param dpi       the DPI (dots per inch) to render at
     * @return the rendered page image
     * @throws IOException if the PDF cannot be read
     */
    public BufferedImage renderImageWithDPI(int pageIndex, float dpi) throws IOException {

        return renderImage(pageIndex, dpi / 72f, ImageType.RGB);
    }

    /**
     * Returns the given page as an RGB image at the given DPI.
     *
     * @param pageIndex the zero-based index of the page to be converted
     * @param dpi       the DPI (dots per inch) to render at
     * @param imageType the type of image to return
     * @return the rendered page image
     * @throws IOException if the PDF cannot be read
     */
    public BufferedImage renderImageWithDPI(int pageIndex, float dpi, ImageType imageType)
            throws IOException {
        return renderImage(pageIndex, dpi / 72f, imageType);
    }

    /**
     * Returns the given page as an RGB or ARGB image at the given scale.
     *
     * @param pageIndex the zero-based index of the page to be converted
     * @param scale     the scaling factor, where 1 = 72 DPI
     * @param imageType the type of image to return
     * @return the rendered page image
     * @throws IOException if the PDF cannot be read
     */
    public BufferedImage renderImage(int pageIndex, float scale, ImageType imageType)
            throws IOException {
        // pageIndex, pdf 中的具体的某一页
        // scale= dpi / 72f, 他是一个值, dpi / 72f
        // imageType=ImageType.RGB
        // RenderDestination.EXPORT
        return renderImage(pageIndex, scale, imageType,
                defaultDestination == null ? RenderDestination.EXPORT : defaultDestination);
    }

    /**
     * Returns the given page as an RGB or ARGB image at the given scale.
     *
     * @param pageIndex   the zero-based index of the page to be converted
     * @param scale       the scaling factor, where 1 = 72 DPI
     * @param imageType   the type of image to return
     * @param destination controlling visibility of optional content groups
     * @return the rendered page image
     * @throws IOException if the PDF cannot be read
     */
    public BufferedImage renderImage(int pageIndex, float scale, ImageType imageType, RenderDestination destination)
            throws IOException {
        //参数
        // pageIndex,                                     pdf 中的具体的某一页          pageIndex = 0
        // scale=dpi / 72f,                               DPI 除以/72                  scale = 2.0
        // imageType=ImageType.RGB                        渲染类型                     imageType = {ImageType$3@1391} "RGB"
        // RenderDestination.EXPORT                       导出                         destination = {RenderDestination@1394} "EXPORT"


        //pdf 中具体的某一页
        PDPage page = document.getPage(pageIndex);

        //pdf 某一页的宽高(pt)
        //cropbBox = {PDRectangle@1402} "[0.0,0.0,960.0,540.0]"
        // rectArray = {COSArray@1404} "COSArray{[COSFloat{0.0}, COSFloat{0.0}, COSFloat{960.0}, COSFloat{540.0}]}"
        //  objects = {ArrayList@1406}  size = 4
        //   0 = {COSFloat@1408} "COSFloat{0.0}"
        //   1 = {COSFloat@1409} "COSFloat{0.0}"
        //   2 = {COSFloat@1410} "COSFloat{960.0}"
        //   3 = {COSFloat@1411} "COSFloat{540.0}"
        //  needToBeUpdated = false
        //  direct = false
        PDRectangle cropbBox = page.getCropBox();


        float widthPt = cropbBox.getWidth();
        float heightPt = cropbBox.getHeight();
        //widthPt = 960.0
        //heightPt = 540.0

        //pdf 某一页的宽高(px)
        // PDFBOX-4306 avoid single blank pixel line on the right or on the bottom
        int widthPx = (int) Math.max(Math.floor(widthPt * scale), 1);
        int heightPx = (int) Math.max(Math.floor(heightPt * scale), 1);
        //widthPx = 1920
        //heightPx = 1080


        //防止出错
        // PDFBOX-4518 the maximum size (w*h) of a buffered image is limited to Integer.MAX_VALUE
        if ((long) widthPx * (long) heightPx > Integer.MAX_VALUE) {
            throw new IOException("Maximum size of image exceeded (w * h * scale) = "//
                    + widthPt + " * " + heightPt + " * " + scale + " > " + Integer.MAX_VALUE);
        }

        // 该页 pdf 的旋转角度
        int rotationAngle = page.getRotation();

        int bimType = imageType.toBufferedImageType();
        if (imageType != ImageType.ARGB && hasBlendMode(page)) {
            // PDFBOX-4095: if the PDF has blending on the top level, draw on transparent background
            // Inpired from PDF.js: if a PDF page uses any blend modes other than Normal, 
            // PDF.js renders everything on a fully transparent RGBA canvas. 
            // Finally when the page has been rendered, PDF.js draws the RGBA canvas on a white canvas.
            bimType = BufferedImage.TYPE_INT_ARGB;
        }

        //根据图像的旋转角度旋转
        // swap width and height
        BufferedImage image;
        if (rotationAngle == 90 || rotationAngle == 270) {
            image = new BufferedImage(heightPx, widthPx, bimType);
        } else {
            image = new BufferedImage(widthPx, heightPx, bimType);
        }

        pageImage = image; //BufferedImage@61f8bee4: type = 1 DirectColorModel: rmask=ff0000 gmask=ff00 bmask=ff amask=0 IntegerInterleavedRaster: width = 1920 height = 1080 #Bands = 3 xOff = 0 yOff = 0 dataOffset[0] 0

        //如果图像类型支持alpha，请使用透明背景
        // use a transparent background if the image type supports alpha
        Graphics2D g = image.createGraphics();
        if (image.getType() == BufferedImage.TYPE_INT_ARGB) { //1
            g.setBackground(new Color(0, 0, 0, 0));
        } else {
            //走这里
            g.setBackground(Color.WHITE);
        }
        //防止 背景图变黑了
        //实践说明 setBackground(Color.WHITE) +  clearRect(0, 0,  image.getWidth(), image.getHeight()) 才能画上背景图，否则一直都黑。
        g.clearRect(0, 0, image.getWidth(), image.getHeight());


        //将图片旋转
        //如果是 rotationAngle = page.getRotation() != 0 , 就执行该函数,
        transform(g, page, scale, scale);

        // the end-user may provide a custom PageDrawer
        RenderingHints actualRenderingHints =
                renderingHints == null ? createDefaultRenderingHints(g) : renderingHints;
        //使用java.awt.RenderingHints类设置参数，改善图片质量

        //RenderingHints 类定义了多种着色微调


        // 键描述KEY_ANTIALIASING决定是否使用抗锯齿。
        // 当着色有倾斜角度的线时，通常会得到一组阶梯式的像素排列，使这条线看上去不平滑，经常被称为 锯齿状图形。
        // 抗锯齿是一种技术，它设置有倾斜角度的线的像素亮度，以使线看起来更平滑。

        //KEY_COLOR_RENDERING控制颜色着色的方式。可能的值有 VALUE_COLOR_RENDER_SPEED,
        //actualRenderingHints = {RenderingHints@1816}  size = 3
        // {SunHints$Key@1828} "Global rendering quality key" -> {SunHints$Value@1829} "Highest quality rendering methods"
        // {SunHints$Key@1830} "Global antialiasing enable key" -> {SunHints$Value@1831} "Antialiased rendering mode"
        // {SunHints$Key@1832} "Image interpolation method key" -> {SunHints$Value@1833} "Bicubic image interpolation mode"


        PageDrawerParameters parameters = new PageDrawerParameters(
                this,  // 当前页
                page, //pdf 中的一页
                subsamplingAllowed, //false
                destination, //  RenderDestination.EXPORT                       导出
                actualRenderingHints  // 设置参数，改善图片质量
        );

        //subsamplingAllowed :   false;
        // destination:          RenderDestination.EXPORT

        PageDrawer drawer = createPageDrawer(parameters);
        drawer.drawPage(g, page.getCropBox());

        g.dispose();

        if (image.getType() != imageType.toBufferedImageType()) {
            // PDFBOX-4095: draw temporary transparent image on white background
            BufferedImage newImage =
                    new BufferedImage(image.getWidth(), image.getHeight(), imageType.toBufferedImageType());
            Graphics2D dstGraphics = newImage.createGraphics();
            dstGraphics.setBackground(Color.WHITE);
            dstGraphics.clearRect(0, 0, image.getWidth(), image.getHeight());
            dstGraphics.drawImage(image, 0, 0, null);
            dstGraphics.dispose();
            image = newImage;
        }

        return image;
    }

    /**
     * Renders a given page to an AWT Graphics2D instance at 72 DPI.
     * <p>
     * Read {@link #renderPageToGraphics(int, java.awt.Graphics2D, float, float, org.apache.pdfbox.rendering.RenderDestination) renderPageToGraphics(int, Graphics2D, float, float, RenderDestination)}
     * before using this.
     *
     * @param pageIndex the zero-based index of the page to be converted
     * @param graphics  the Graphics2D on which to draw the page
     * @throws IOException if the PDF cannot be read
     */
    public void renderPageToGraphics(int pageIndex, Graphics2D graphics) throws IOException {
        renderPageToGraphics(pageIndex, graphics, 1);
    }

    /**
     * Renders a given page to an AWT Graphics2D instance.
     * <p>
     * Read {@link #renderPageToGraphics(int, java.awt.Graphics2D, float, float, org.apache.pdfbox.rendering.RenderDestination) renderPageToGraphics(int, Graphics2D, float, float, RenderDestination)}
     * before using this.
     *
     * @param pageIndex the zero-based index of the page to be converted
     * @param graphics  the Graphics2D on which to draw the page
     * @param scale     the scaling factor, where 1 = 72 DPI
     * @throws IOException if the PDF cannot be read
     */
    public void renderPageToGraphics(int pageIndex, Graphics2D graphics, float scale)
            throws IOException {
        renderPageToGraphics(pageIndex, graphics, scale, scale);
    }

    /**
     * Renders a given page to an AWT Graphics2D instance.
     * <p>
     * Read {@link #renderPageToGraphics(int, java.awt.Graphics2D, float, float, org.apache.pdfbox.rendering.RenderDestination) renderPageToGraphics(int, Graphics2D, float, float, RenderDestination)}
     * before using this.
     *
     * @param pageIndex the zero-based index of the page to be converted
     * @param graphics  the Graphics2D on which to draw the page
     * @param scaleX    the scale to draw the page at for the x-axis, where 1 = 72 DPI
     * @param scaleY    the scale to draw the page at for the y-axis, where 1 = 72 DPI
     * @throws IOException if the PDF cannot be read
     */
    public void renderPageToGraphics(int pageIndex, Graphics2D graphics, float scaleX, float scaleY)
            throws IOException {
        renderPageToGraphics(pageIndex, graphics, scaleX, scaleY,
                defaultDestination == null ? RenderDestination.VIEW : defaultDestination);
    }

    /**
     * Renders a given page to an AWT Graphics2D instance.
     * <p>
     * Known problems:
     * <ul>
     * <li>rendering of PDF files with transparencies is not supported on Ubuntu, see
     * <a href="https://issues.apache.org/jira/browse/PDFBOX-4581">PDFBOX-4581</a> and
     * <a href="https://bugs.openjdk.java.net/browse/JDK-6689349">JDK-6689349</a>. Rendering will
     * not abort, but the pages will be rendered incorrectly.</li>
     * <li>Clipping the Graphics2D will not work properly, see
     * <a href="https://issues.apache.org/jira/browse/PDFBOX-4583">PDFBOX-4583</a>.</li>
     * </ul>
     * If you encounter these problems, then you should render into an image by using the
     * {@link #renderImage(int) renderImage} methods.
     *
     * @param pageIndex   the zero-based index of the page to be converted
     * @param graphics    the Graphics2D on which to draw the page
     * @param scaleX      the scale to draw the page at for the x-axis, where 1 = 72 DPI
     * @param scaleY      the scale to draw the page at for the y-axis, where 1 = 72 DPI
     * @param destination controlling visibility of optional content groups
     * @throws IOException if the PDF cannot be read
     */
    public void renderPageToGraphics(int pageIndex, Graphics2D graphics, float scaleX, float scaleY, RenderDestination destination)
            throws IOException {
        PDPage page = document.getPage(pageIndex);
        // TODO need width/height calculations? should these be in PageDrawer?

        transform(graphics, page, scaleX, scaleY);

        PDRectangle cropBox = page.getCropBox();
        graphics.clearRect(0, 0, (int) cropBox.getWidth(), (int) cropBox.getHeight());

        // the end-user may provide a custom PageDrawer
        RenderingHints actualRenderingHints =
                renderingHints == null ? createDefaultRenderingHints(graphics) : renderingHints;
        PageDrawerParameters parameters = new PageDrawerParameters(this, page, subsamplingAllowed,
                destination, actualRenderingHints);
        PageDrawer drawer = createPageDrawer(parameters);
        drawer.drawPage(graphics, cropBox);
    }

    /**
     * Indicates whether an optional content group is enabled.
     *
     * @param group the group
     * @return true if the group is enabled
     */
    public boolean isGroupEnabled(PDOptionalContentGroup group) {
        PDOptionalContentProperties ocProperties = document.getDocumentCatalog().getOCProperties();
        return ocProperties == null || ocProperties.isGroupEnabled(group);
    }

    // scale rotate translate
    private void transform(Graphics2D graphics, PDPage page, float scaleX, float scaleY) {
        //graphics 调用函数传过来的画布 = {SunGraphics2D@1676} "sun.java2d.SunGraphics2D[font=java.awt.Font[family=Dialog,name=Dialog,style=plain,size=12],color=java.awt.Color[r=255,g=255,b=255]]"
        //page pdf 中具体的某一页, = {PDPage@1399}
        //scaleX = 2.0
        //scaleY = 2.0
        graphics.scale(scaleX, scaleY);

        // TODO should we be passing the scale to PageDrawer rather than messing with Graphics?
        int rotationAngle = page.getRotation();
        PDRectangle cropBox = page.getCropBox();

        if (rotationAngle != 0) {
            float translateX = 0;
            float translateY = 0;
            switch (rotationAngle) {
                case 90:
                    translateX = cropBox.getHeight();
                    break;
                case 270:
                    translateY = cropBox.getWidth();
                    break;
                case 180:
                    translateX = cropBox.getWidth();
                    translateY = cropBox.getHeight();
                    break;
                default:
                    break;
            }
            graphics.translate(translateX, translateY);
            graphics.rotate(Math.toRadians(rotationAngle));
        }
    }

    private boolean isBitonal(Graphics2D graphics) {
        GraphicsConfiguration deviceConfiguration = graphics.getDeviceConfiguration();
        if (deviceConfiguration == null) {
            return false;
        }
        GraphicsDevice device = deviceConfiguration.getDevice();
        if (device == null) {
            return false;
        }
        DisplayMode displayMode = device.getDisplayMode();
        if (displayMode == null) {
            return false;
        }
        return displayMode.getBitDepth() == 1;
    }

    private RenderingHints createDefaultRenderingHints(Graphics2D graphics) {
        RenderingHints r = new RenderingHints(null);
        r.put(RenderingHints.KEY_INTERPOLATION, isBitonal(graphics) ?
                RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR :
                RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        r.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        r.put(RenderingHints.KEY_ANTIALIASING, isBitonal(graphics) ?
                RenderingHints.VALUE_ANTIALIAS_OFF :
                RenderingHints.VALUE_ANTIALIAS_ON);
        return r;
    }

    /**
     * Returns a new PageDrawer instance, using the given parameters. May be overridden.
     */
    protected PageDrawer createPageDrawer(PageDrawerParameters parameters) throws IOException {
        PageDrawer pageDrawer = new PageDrawer(parameters);
        pageDrawer.setAnnotationFilter(annotationFilter);
        return pageDrawer;
    }

    private boolean hasBlendMode(PDPage page) {
        // check the current resources for blend modes
        PDResources resources = page.getResources();
        if (resources == null) {
            return false;
        }
        for (COSName name : resources.getExtGStateNames()) {
            PDExtendedGraphicsState extGState = resources.getExtGState(name);
            if (extGState == null) {
                // can happen if key exists but no value 
                // see PDFBOX-3950-23EGDHXSBBYQLKYOKGZUOVYVNE675PRD.pdf
                continue;
            }
            BlendMode blendMode = extGState.getBlendMode();
            if (blendMode != BlendMode.NORMAL) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the image to which the current page is being rendered.
     * May be null if the page is rendered to a Graphics2D object
     * instead of a BufferedImage.
     */
    BufferedImage getPageImage() {
        return pageImage;
    }

    private static void suggestKCMS() {
        String cmmProperty = System.getProperty("sun.java2d.cmm");
        if (!"sun.java2d.cmm.kcms.KcmsServiceProvider".equals(cmmProperty)) {
            try {
                // Make sure that class exists
                Class.forName("sun.java2d.cmm.kcms.KcmsServiceProvider");

                String version = System.getProperty("java.version");
                if (version == null ||
                        isGoodVersion(version, "1.8.0_(\\d+)", 191) ||
                        isGoodVersion(version, "9.0.(\\d+)", 4)) {
                    return;
                }
                LOG.info("Your current java version is: " + version);
                LOG.info("To get higher rendering speed on old java 1.8 or 9 versions,");
                LOG.info("  update to the latest 1.8 or 9 version (>= 1.8.0_191 or >= 9.0.4),");
                LOG.info("  or");
                LOG.info("  use the option -Dsun.java2d.cmm=sun.java2d.cmm.kcms.KcmsServiceProvider");
                LOG.info("  or call System.setProperty(\"sun.java2d.cmm\", \"sun.java2d.cmm.kcms.KcmsServiceProvider\")");
            } catch (ClassNotFoundException e) {
                // KCMS not available
            }
        }
    }

    private static boolean isGoodVersion(String version, String regex, int min) {
        Matcher matcher = Pattern.compile(regex).matcher(version);
        if (matcher.matches() && matcher.groupCount() >= 1) {
            try {
                int v = Integer.parseInt(matcher.group(1));
                if (v >= min) {
                    // LCMS no longer bad
                    return true;
                }
            } catch (NumberFormatException ex) {
                return true;
            }
        }
        return false;
    }
}
